#include <QApplication>
#include "cutter.h"

//! Constructor.
Cutter::Cutter(QWidget *parent)
    : QDialog(parent), ui(new Ui::CutterClass)
{
    ui->setupUi(this);

    // Allocate the context menu.
    listContextMenu = new QMenu(this);

    // Initialize seekPending-flag.
    seekPending=false;

    // Deferred init for the rest.
    QTimer::singleShot( 0, this, SLOT(deferredInit()) );
}


// Deferred initialization for the rest of UI.
void Cutter::deferredInit()
{
    TRACE("Deferred init called.");
    // Initialize stuff.
    setAcceptDrops(true);
    mediaPlayer = new QMediaPlayer;
    mediaPlayer->setVideoOutput(ui->videoWidget);

    // Set slider values.
    ui->seekSlider->setMinimum(0);
    ui->seekSlider->setMaximum(100);

    // Set model for the view.
    model.setColumnCount(2);
    ui->fileList->setModel(&model);

    // Set jump forwards/backwards icons in place.
    ui->jumpBack->setIcon(style()->standardIcon(QStyle::SP_ArrowLeft));
    ui->jumpForward->setIcon(style()->standardIcon(QStyle::SP_ArrowRight));

    // Set play/pause to be a toggle button.
    ui->playPause->setCheckable(true);
    ui->playPause->setIcon(style()->standardIcon(QStyle::SP_MediaPlay));
    ui->playPause->setDisabled(true);

    // Connect signals: wait for video change signal.
    connect(mediaPlayer, SIGNAL(stateChanged(QMediaPlayer::State)), this, SLOT(stateChanged(QMediaPlayer::State)));
    connect(mediaPlayer, SIGNAL(positionChanged(qint64)), this, SLOT(tick(qint64)));
    connect(mediaPlayer, SIGNAL(durationChanged(qint64)), this, SLOT(mediaDurationChanged(qint64)));

    // File list signals.
    connect(ui->fileList, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(listDoubleClicked(QModelIndex)));
    connect(ui->fileList, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(customContextMenuRequested(QPoint)));

    // Slider signals.
    connect(ui->seekSlider, SIGNAL(sliderPressed()), this, SLOT(sliderPressed()));
    connect(ui->seekSlider, SIGNAL(sliderReleased()), this, SLOT(sliderReleased()));

    /*
      Add actions to the menu: keep addition order the same
      as the 'moveUp' ... enumeration in the class header. */
    contextMenuActions.append(listContextMenu->addAction(tr("&Load as active")));
    connect(contextMenuActions.last(), SIGNAL(triggered()), this, SLOT(contextMenuLoad()));
    listContextMenu->addSeparator();
    contextMenuActions.append(listContextMenu->addAction(tr("Move &up")));
    connect(contextMenuActions.last(), SIGNAL(triggered()), this, SLOT(contextMenuMoveUp()));
    contextMenuActions.append(listContextMenu->addAction(tr("Move &down")));
    connect(contextMenuActions.last(), SIGNAL(triggered()), this, SLOT(contextMenuMoveDown()));
    listContextMenu->addSeparator();
    contextMenuActions.append(listContextMenu->addAction(tr("&Add to list...")));
    connect(contextMenuActions.last(), SIGNAL(triggered()), this, SLOT(contextMenuAdd()));
    contextMenuActions[Add]->setEnabled(true);
    contextMenuActions.append(listContextMenu->addAction(tr("&Remove from list")));
    connect(contextMenuActions.last(), SIGNAL(triggered()), this, SLOT(contextMenuRemove()));
    contextMenuActions.append(listContextMenu->addAction(tr("&Clear list")));
    connect(contextMenuActions.last(), SIGNAL(triggered()), this, SLOT(contextMenuClear()));

    // Preload the list with files from the command line.
    QStringList commandLine(qApp->arguments());
    commandLine.removeFirst();
    foreach(QString item, commandLine)
    {
        QUrl url(QUrl::fromLocalFile(item));
        loadListWithVideoUrl(url);
    }

    // Now show the window.
    show();
}

//! Destructor.
Cutter::~Cutter()
{
    delete listContextMenu;
    delete ui;
    delete mediaPlayer;
}

//! Video advance tick.
void Cutter::tick(qint64 time)
{
    // Update current position on UI.
    ui->currentTime->setText(printableMilliseconds(time));
    if(!seekPending)
    {
        qint64 duration = mediaPlayer->duration();
        double progress = 100.0*time/duration;
        ui->seekSlider->setValue((int)progress);
    }
}

//! State change from the mediaObject.
void Cutter::stateChanged(QMediaPlayer::State newState)
{
    QString newStateString;
    qint64 totalTime;
    switch(newState)
    {
    case QMediaPlayer::StoppedState:
        // Loading is done. Set time fields from model data.
        newStateString.append(tr("Ready."));
        totalTime = mediaPlayer->duration();
        ui->totalTime->setText(printableMilliseconds(totalTime));
        setDataTimeValue(Cutter::TotalLength, totalTime);
        ui->currentTime->setText(printableMilliseconds(dataTimeValue(Cutter::CurrentPosition)));
        ui->aPoint->setText(printableMilliseconds(dataTimeValue(Cutter::APosition)));
        ui->bPoint->setText(printableMilliseconds(dataTimeValue(Cutter::BPosition)));
        // Raise seekPending-flag if current position is something else than zero.
        if(dataTimeValue(Cutter::CurrentPosition)!=UNDEFINED_CURRENT_POSITION)
        {
            seekPending=true;
            // Also, start playing to enable seek slider to update its position.
            mediaPlayer->play();
            ui->playPause->setIcon(style()->standardIcon(QStyle::SP_MediaPause));
        }
        else
        {
            seekPending=false;
            // Enter pause state to enable cut point editing.
            mediaPlayer->pause();
            ui->playPause->setIcon(style()->standardIcon(QStyle::SP_MediaPlay));
        }
        break;
    case QMediaPlayer::PlayingState:
        newStateString.append(tr("Playing..."));
        if(seekPending)
        {
            // Clear the flag ...
            seekPending=false;
            // ... jump to stored position, and ...
            mediaPlayer->setPosition(dataTimeValue(Cutter::CurrentPosition));
            // pause. Now slider is in correct position.
            mediaPlayer->pause();
            ui->playPause->setIcon(style()->standardIcon(QStyle::SP_MediaPlay));
        }
        else
        {
            ui->playPause->setIcon(style()->standardIcon(QStyle::SP_MediaPause));
        }
        break;
    case QMediaPlayer::PausedState:
        newStateString.append(tr("Paused state."));
        ui->playPause->setIcon(style()->standardIcon(QStyle::SP_MediaPlay));
        break;
    default:
        newStateString.append(tr("Unknown state."));
        break;
    }
    TRACE(newStateString);
    msg(newStateString);
}

//! Update the total time -field.
void Cutter::mediaDurationChanged(qint64 newDuration)
{
    ui->playPause->setDisabled(false);
    ui->seekSlider->setValue(0);
    newDuration = mediaPlayer->duration();
    TRACE(QString("New duration: %1 ms").arg(newDuration));
    ui->totalTime->setText(printableMilliseconds(newDuration));
}

//! Get value from a widget.
qint64 Cutter::lineEditTimeValue(QLineEdit *lineEdit)
{
    if(lineEdit!=NULL)
    {
        QString lineString = lineEdit->text();
        TRACE(lineString);
        QStringList elements = lineString.split(' ', QString::SkipEmptyParts);
        if(elements.size())
        {
            QString valueString = elements.at(0);
            TRACE(valueString);
            if(!valueString.isEmpty())
            {
                bool conversionOk;
                return valueString.toLongLong(&conversionOk);
            }
        }
        else
        {
            return Q_INT64_C(0);
        }
    }
    return 0;
}

//! Store A point.
void Cutter::on_setCutA_clicked()
{
    qint64 timeNow = mediaPlayer->position();
    setDataTimeValue(Cutter::APosition, timeNow);
    ui->aPoint->setText(printableMilliseconds(timeNow));
}

//! Store B point.
void Cutter::on_setCutB_clicked()
{
    qint64 timeNow = mediaPlayer->position();
    setDataTimeValue(Cutter::BPosition, timeNow);
    ui->bPoint->setText(printableMilliseconds(timeNow));
}

//! Jump to A point.
void Cutter::on_jumpToA_clicked()
{
    mediaPlayer->setPosition(dataTimeValue(Cutter::APosition));
}

//! Jump to B point.
void Cutter::on_jumpToB_clicked()
{
    mediaPlayer->setPosition(dataTimeValue(Cutter::BPosition));
}

//! Play/pause the video.
void Cutter::on_playPause_clicked()
{
    if(mediaPlayer->state()!=QMediaPlayer::PlayingState)
    {
        TRACE("mediaObject->play();");
        mediaPlayer->play();
    }
    else
    {
        TRACE("mediaObject->pause();");
        mediaPlayer->pause();
    }
}

//! Initially all dropped items are accepted.
void Cutter::dragEnterEvent(QDragEnterEvent *event)
{
    event->acceptProposedAction();
}

//! Load image from dropped item.
void Cutter::dropEvent(QDropEvent *event)
{
    QList<QUrl> urls = event->mimeData()->urls();
    foreach(QUrl url, urls)
    {
        loadListWithVideoUrl(url);
    }
}

//! Load a video by its url into the list.
void Cutter::loadListWithVideoUrl(const QUrl &url)
{
    QStandardItem *name;
    QStandardItem *fullPath;
    QStandardItem *undefinedCurrentPosition;
    QStandardItem *undefinedAPosition;
    QStandardItem *undefinedBPosition;
    QStandardItem *undefinedTotalTime = new QStandardItem();
    QFileInfo urlInfo(url.toLocalFile());
    QList<QStandardItem *> items;
    name = new QStandardItem(urlInfo.fileName());
    name->setEditable(false);
    name->setToolTip(urlInfo.absoluteFilePath());
    fullPath = new QStandardItem(urlInfo.absoluteFilePath());
    undefinedCurrentPosition = new QStandardItem();
    undefinedCurrentPosition->setData(QVariant(UNDEFINED_CURRENT_POSITION));
    undefinedAPosition = new QStandardItem();
    undefinedAPosition->setData(QVariant(UNDEFINED_A_POSITION));
    undefinedBPosition = new QStandardItem();
    undefinedBPosition->setData(QVariant(UNDEFINED_B_POSITION));
    undefinedTotalTime->setData(QVariant(UNDEFINED_TOTAL_TIME));
    items << name << fullPath << undefinedCurrentPosition <<
             undefinedAPosition << undefinedBPosition << undefinedTotalTime;
    model.appendRow(items);
    items.clear();
}

//! List was double clicked, select the clicked item as current video.
void Cutter::listDoubleClicked( const QModelIndex & index )
{
    // Get the file's name and full path. Load it via single shot timer.
    QModelIndex pathIndex = index.sibling(index.row(), Cutter::AbsoluteFilePath);
    if(pathIndex.isValid())
    {
        // Store current video's position to model.
        setDataTimeValue(Cutter::CurrentPosition, mediaPlayer->position());
        mediaPlayer->stop();
        ui->playPause->setIcon(style()->standardIcon(QStyle::SP_MediaPlay));

        // Load new video's name and path. Set up the new video for playback.
        QString videoPath = model.item(index.row(), Cutter::AbsoluteFilePath)->text();
        QString videoName = model.item(index.row(), Cutter::FileName)->text();
        TRACE(QString("Name: ")+videoName);
        TRACE(QString("Path: ")+videoPath);
        ui->videoWidget->setToolTip(videoPath);
        QTimer::singleShot(0, this, SLOT(loadVideo()));
    }
}

//! Load a video. Filename is stored in video widget's tooltip.
void Cutter::loadVideo()
{
    TRACE("Cutter::loadVideo()");
    mediaContent.reset(new QMediaContent(QUrl::fromLocalFile(ui->videoWidget->toolTip())));
    mediaPlayer->setMedia(*mediaContent);
}

//! Pop up a custom menu for deleting an item.
void Cutter::customContextMenuRequested( const QPoint & pos )
{
    if(model.rowCount())
    {
        // Disable move up when first item is selected.
        if(ui->fileList->currentIndex().row()==0)
        {
            contextMenuActions[MoveUp]->setDisabled(true);
        }
        else
        {
            contextMenuActions[MoveUp]->setEnabled(true);
        }
        // Disable move down when last item is selected.
        if(ui->fileList->currentIndex().row()==model.rowCount()-1)
        {
            contextMenuActions[MoveDown]->setDisabled(true);
        }
        else
        {
            contextMenuActions[MoveDown]->setEnabled(true);
        }
    }
    else
    {
        foreach(QAction *action, contextMenuActions)
        {
            if(action!=contextMenuActions[Add])
            {
                action->setDisabled(true);
            }
        }
    }
    listContextMenu->move( mapToGlobal(pos) );
    listContextMenu->exec();
}

//! Context menu item was triggered.
void Cutter::contextMenuLoad()
{
    listDoubleClicked(ui->fileList->currentIndex());
}

//! Context menu item was triggered.
void Cutter::contextMenuMoveDown()
{
    int currentRow = ui->fileList->currentIndex().row();
    QList<QStandardItem *> currentRowData = model.takeRow(currentRow);
    model.insertRow(currentRow+1, currentRowData);
}

//! Context menu item was triggered.
void Cutter::contextMenuMoveUp()
{
    int currentRow = ui->fileList->currentIndex().row();
    QList<QStandardItem *> currentRowData = model.takeRow(currentRow);
    model.insertRow(currentRow-1, currentRowData);
}

//! Context menu item was triggered.
void Cutter::contextMenuAdd()
{
    TRACE("Add an item/items to the list...");
    QStringList files = QFileDialog::getOpenFileNames(
                this,
                "Select one or more files to open",
                "",
                "All files (*.*)");
    foreach (QString filename, files)
    {
        loadListWithVideoUrl(QUrl::fromLocalFile(filename));
    }
}

//! Context menu item was triggered.
void Cutter::contextMenuRemove()
{
    model.removeRow(ui->fileList->currentIndex().row());
    if(!model.rowCount())
    {
        ui->playPause->setIcon(style()->standardIcon(QStyle::SP_MediaPlay));
        ui->playPause->setDisabled(true);
    }
}

//! Context menu item was triggered.
void Cutter::contextMenuClear()
{
    ui->playPause->setIcon(style()->standardIcon(QStyle::SP_MediaPlay));
    ui->playPause->setDisabled(true);
    model.clear();
}

//! Set current video's data model row's one time variable to something.
void Cutter::setDataTimeValue(Cutter::ModelColumn columnIndex, qint64 timeValue)
{
    Q_ASSERT(columnIndex>=Cutter::CurrentPosition);

    // Find model row using absolute file path.
    QList<QStandardItem *> itemList = model.findItems(ui->videoWidget->toolTip(),
                                                      Qt::MatchFixedString,
                                                      Cutter::AbsoluteFilePath);
    if(itemList.size())
    {
        // Get wanted column item from the row.
        QStandardItem *item = model.item(itemList[0]->row(), columnIndex);
        // Set the time value.
        item->setData(QVariant(timeValue));
    }
}

//! Get data model row's one time variable for current video.
qint64 Cutter::dataTimeValue(Cutter::ModelColumn columnIndex)
{
    Q_ASSERT(columnIndex>=Cutter::CurrentPosition);

    // Find model row using absolute file path.
    QList<QStandardItem *> itemList = model.findItems(ui->videoWidget->toolTip(),
                                                      Qt::MatchFixedString,
                                                      Cutter::AbsoluteFilePath);
    if(itemList.size())
    {
        // Get wanted column item from the row.
        QStandardItem *item = model.item(itemList[0]->row(), columnIndex);
        // Return the time value.
        return item->data().toLongLong();
    }
    else
    {
        return Q_INT64_C(0);
    }
}

//! Get a string list containing command lines to be saved as a shell script.
QStringList Cutter::createJoblist()
{
    // Run through rows and generate command lines to run from each line.
    QStringList joblist;
    QString sourceFile;
    QFileInfo targetFileInfo;
    qint64 alpha, omega;
    for(int rowIndex=0; rowIndex < model.rowCount(); rowIndex++)
    {
        // Get source file's absolute path.
        sourceFile = model.item(rowIndex, Cutter::AbsoluteFilePath)->text();
        // Get A-point's value.
        alpha = model.item(rowIndex, Cutter::APosition)->data().toLongLong()/ONE_SECOND_IN_MILLIS;
        // Get B-point's value.
        omega = model.item(rowIndex, Cutter::BPosition)->data().toLongLong()/ONE_SECOND_IN_MILLIS;
        // Make sure alpha is the smaller of the two.
        if(alpha>omega)
        {
            qSwap(alpha, omega);
        }
        // Sanity check #1: if alpha is -1, then it must be 0, i.e. file start.
        if(alpha<0)
        {
            alpha=0;
        }
        // Sanity check #2: if omega is -1, then it must be the length of the video.
        if(omega<0)
        {
            // TODO: Figure out the length of the video.
            omega = millisecondsIntoSeconds(dataTimeValue(Cutter::TotalLength));
        }
        // Round omega up to the next second.
        omega++;
        // Build target file's name from source file.
        targetFileInfo.setFile(sourceFile);
        targetFileInfo.setFile(targetFileInfo.dir(), "cut_"+targetFileInfo.fileName());
        // Store line into string buffer.
        joblist.append(QString("MP4Box -splitx %1:%2 -out \"%3\" \"%4\"").arg(alpha).arg(omega).arg(targetFileInfo.absoluteFilePath()).arg(sourceFile));
    }
    return joblist;
}

//! Store the edit list as a file.
void Cutter::on_saveScript_clicked()
{
    // Get joblist.
    QStringList joblist = createJoblist();

    // Abort if list is empty.
    if(joblist.isEmpty())
    {
        msg(tr("No jobs to be saved into shell script, aborting."));
        return;
    }

    // Save joblist into a file.
    QString fileName = QFileDialog::getSaveFileName(this,
                                                    tr("Save As Shell Script"),
                                                    ".",
                                                    tr("Shell script (*.sh)"));
    if(fileName.isEmpty())
    {
        msg(tr("Shell script save cancelled by user."));
        return;
    }

    // Store joblist into a file.
    QFile outFile(fileName);
    if(!outFile.open(QFile::WriteOnly | QFile::Text))
    {
        msg(tr("Failed to save the job list to a file!"));
        return;
    }
    QTextStream outStream(&outFile);
    foreach(QString job, joblist)
    {
        outStream << job << endl;
    }
    msg(tr("Shell script successfully saved."));
}

//! Free resources used by video playback devices.
void Cutter::freeVideoResources()
{
    TRACE("freeVideoResources");
}

//! Capture close event to free video resources.
void Cutter::closeEvent(QCloseEvent *event)
{
    TRACE("closeEvent");
    freeVideoResources();
    if(event!=0)
    {
        event->accept();
    }
}

//! Capture ESC-key.
void Cutter::reject()
{
    TRACE("reject, redirect to closeEvent");
    freeVideoResources();
    done(Rejected);
}

//! Jump forwards.
void Cutter::on_jumpForward_clicked()
{
    mediaPlayer->setPosition(mediaPlayer->position()+5000);
}

//! Jump backwards.
void Cutter::on_jumpBack_clicked()
{
    mediaPlayer->setPosition(mediaPlayer->position()-5000);
}

//! Entry point.
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    a.setApplicationName("Cutter");
    Cutter w;
    return a.exec();
}

// End of file.
